﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Data;

namespace Granite.Data
{

    /// <summary>
    /// Abstract representation of a RPC-style connection. Generally this will be a database connection.
    /// </summary>
    public abstract class Connection
    {
        /// <summary>
        /// Raised when a procedure call fails.
        /// </summary>
        public event EventHandler<ProcedureCallEventArgs> ProcedureCallError;

        /// <summary>
        /// Raised when a procedure call is successfully completed
        /// </summary>
        public event EventHandler<ProcedureCallEventArgs> ProcedureCallFinished;

        /// <summary>
        /// Raised when a procedure call is started
        /// </summary>
        public event EventHandler<ProcedureCallEventArgs> ProcedureCallStarted;

        /// <summary>
        /// Executes the procedure, returning a single non-null value.
        /// </summary>
        /// <param name="procedure"></param>
        /// <returns></returns>
		public bool ExecuteBoolean(ProcedureCall procedure)
		{
			if (procedure == null)
				throw new ArgumentNullException("procedure", "procedure is null.");
			Contract.EndContractBlock();

			var result = ExecuteScalar<bool>(procedure);
			if(result.HasValue)
				return result.Value;
			else
				throw new DataException("An unexpected null was returned by the database");
		}

        /// <summary>
        /// Executes the procedure, returning a single non-null value.
        /// </summary>
        /// <param name="procedure"></param>
        /// <returns></returns>
		public DateTime ExecuteDateTime(ProcedureCall procedure)
		{
			if (procedure == null)
				throw new ArgumentNullException("procedure", "procedure is null.");
			Contract.EndContractBlock();

			return ExecuteScalar<DateTime>(procedure).Value;
		}

        /// <summary>
        /// Executes the procedure, returning a single non-null value.
        /// </summary>
        /// <param name="procedure"></param>
        /// <returns></returns>
		public decimal ExecuteDecimal(ProcedureCall procedure)
		{
			if (procedure == null)
				throw new ArgumentNullException("procedure", "procedure is null.");
			Contract.EndContractBlock();

			return ExecuteScalar<decimal>(procedure).Value;
		}

        /// <summary>
        /// Executes the procedure, returning a single non-null value.
        /// </summary>
        /// <param name="procedure"></param>
        /// <returns></returns>
		public short ExecuteInt16(ProcedureCall procedure)
		{
			if (procedure == null)
				throw new ArgumentNullException("procedure", "procedure is null.");
			Contract.EndContractBlock();

			return ExecuteScalar<short>(procedure).Value;
		}

        /// <summary>
        /// Executes the procedure, returning a single non-null value.
        /// </summary>
        /// <param name="procedure"></param>
        /// <returns></returns>
		public int ExecuteInt32(ProcedureCall procedure)
		{
			if (procedure == null)
				throw new ArgumentNullException("procedure", "procedure is null.");
			Contract.EndContractBlock();

			return ExecuteScalar<int>(procedure).Value;
		}

        /// <summary>
        /// Executes the procedure, discarding any results
        /// </summary>
        /// <param name="procedure"></param>
        /// <returns></returns>
        public abstract void ExecuteNonQuery(ProcedureCall procedure);

        /// <summary>
        /// Executes the procedure, returning a single row.
        /// </summary>
        /// <param name="procedure"></param>
        /// <param name="options">Controls what happens if the wrong number of rows is returned</param>
        /// <returns></returns>
        public abstract IDictionary<string, object> ExecuteRow(ProcedureCall procedure, ExecuteRowOptions options);

        /// <summary>
        /// Executes the procedure, returning a single row.
        /// </summary>
        /// <param name="procedure"></param>
        /// <returns></returns>
		public IDictionary<string, object> ExecuteRow(ProcedureCall procedure)
		{
			if (procedure == null)
				throw new ArgumentNullException("procedure", "procedure is null.");
			Contract.EndContractBlock();

			return ExecuteRow(procedure, ExecuteRowOptions.None);
		}

        /// <summary>
        /// Executes the procedure, returning a single nullable value.
        /// </summary>
        /// <param name="procedure"></param>
        /// <returns></returns>
        public abstract T? ExecuteScalar<T>(ProcedureCall procedure) where T : struct;

        /// <summary>
        /// Executes the procedure, returning a single nullable value.
        /// </summary>
        /// <param name="procedure"></param>
        /// <returns></returns>
        public abstract string ExecuteString(ProcedureCall procedure);

        /// <summary>
        /// Executes the procedure, returning a single Table.
        /// </summary>
        /// <param name="procedure"></param>
        /// <returns></returns>
        public abstract Table ExecuteTable(ProcedureCall procedure);

		/// <summary>
		/// Executes the procedure, returning a list of Tables.
		/// </summary>
		/// <param name="procedure"></param>
		/// <returns></returns>
		public abstract IList<Table> ExecuteTables(ProcedureCall procedure);

        /// <summary>
        /// Executes the procedure, returning a single non-null value.
        /// </summary>
        /// <param name="procedure"></param>
        /// <returns></returns>
        public TimeSpan ExecuteTimeSpan(ProcedureCall procedure)
        {
            return ExecuteScalar<TimeSpan>(procedure).Value;
        }

        /// <summary>
        /// Triggers the ProcedureCallError event.
        /// </summary>
		protected void OnProcedureCallError(ProcedureCall procedure, DateTimeOffset startTime, DateTimeOffset? endTime, Exception error)
		{
			if (procedure == null)
				throw new ArgumentNullException("procedure", "procedure is null.");
			if (error == null)
				throw new ArgumentNullException("error", "error is null.");
			Contract.EndContractBlock();

			if (ProcedureCallError != null)
				ProcedureCallError(this, new ProcedureCallEventArgs(procedure, startTime, endTime, error));
		}

        /// <summary>
        /// Triggers the ProcedureCallFinished event.
        /// </summary>
		protected void OnProcedureCallFinished(ProcedureCall procedure, DateTimeOffset startTime, DateTimeOffset endTime)
		{
			if (procedure == null)
				throw new ArgumentNullException("procedure", "procedure is null.");
			Contract.EndContractBlock();

			if (ProcedureCallFinished != null)
				ProcedureCallFinished(this, new ProcedureCallEventArgs(procedure, startTime, endTime));
		}

        /// <summary>
        /// Triggers the ProcedureCallStarted event.
        /// </summary>
		protected void OnProcedureCallStarted(ProcedureCall procedure, DateTimeOffset startTime)
		{
			if (procedure == null)
				throw new ArgumentNullException("procedure", "procedure is null.");
			Contract.EndContractBlock();

			if (ProcedureCallStarted != null)
				ProcedureCallStarted(this, new ProcedureCallEventArgs(procedure, startTime));
		}


        /// <summary>
        /// Triggers the ProcedureCallError event.
        /// </summary>
        /// <remarks>Used to forward events from transational connections to the original connection</remarks>
        [Browsable(false)]
		public void OnProcedureCallError(ProcedureCallEventArgs args)
		{
			if (args == null)
				throw new ArgumentNullException("args", "args is null.");
			Contract.EndContractBlock();

			if (ProcedureCallError != null)
				ProcedureCallError(this, args);
		}

        /// <summary>
        /// Triggers the ProcedureCallFinished event.
        /// </summary>
        /// <remarks>Used to forward events from transational connections to the original connection</remarks>
        [Browsable(false)]
		public void OnProcedureCallFinished(ProcedureCallEventArgs args)
		{
			if (args == null)
				throw new ArgumentNullException("args", "args is null.");
			Contract.EndContractBlock();

			if (ProcedureCallFinished != null)
				ProcedureCallFinished(this, args);
		}

        /// <summary>
        /// Triggers the ProcedureCallStarted event.
        /// </summary>
        /// <remarks>Used to forward events from transational connections to the original connection</remarks>
        [Browsable(false)]
		public void OnProcedureCallStarted(ProcedureCallEventArgs args)
		{
			if (args == null)
				throw new ArgumentNullException("args", "args is null.");
			Contract.EndContractBlock();

			if (ProcedureCallStarted != null)
				ProcedureCallStarted(this, args);
		}

        /// <summary>
        /// Indicates if transaction support is available
        /// </summary>
        public abstract bool CanBeginTransaction { get; }


        /// <summary>
        /// Creates a new transaction
        /// </summary>
        /// <param name="forwardEvents">If true, logging events are forwarded to the parent connection. </param>
        /// <returns></returns>
        public abstract ITransactionalConnection BeginTransaction(bool forwardEvents = true);

    }
}
